package edu.princeton.cogsci.wn.ascii;

import edu.princeton.cogsci.wn.*;
import java.util.*;
import java.io.*;
import gnu.regexp.*;

/** This class provides the functionality of the WordNet database, using the
 *  old-style ASCII database format. It uses the original C libraries and
 *  new native glue code in the CSynset and CIndex classes to transfer
 *  information from C Synset structs to Java Synset objects.
 *  <P>
 *  This class is designed to be read-only because the ASCII-format database
 *  cannot be edited in real-time. (This limitation exists because the database
 *  is indexed by physical byte offsets.)
 */

public class AsciiWordNetImpl extends WordNetImpl
                                        implements Constants
{

  /** Keeps track of the WordSenseKey objects that we know about. The keys to
   *  this hashtable are strings (concatenations of word, pos, and sense) and
   *  the values are WordSenseKey objects.
   */
  protected static Hashtable wordSenseKeyTable = new Hashtable( );

  /** Keeps track of the synset key objects that we know about. The keys to
   *  this hashtable are strings (concatenations of word, pos, and sense) and
   *  the values are SynsetKey objects.
   */
  protected static Hashtable synsetKeyTable = new Hashtable( );

  /** Keeps track of the word sense objects that we know about. The keys are
   *  WordSenseKey objects and the values are WordSense objects.
   */
  protected static Hashtable wordSenseTable = new Hashtable( );

  /** Keeps track of the synset objects that we know about. The keys are
   *  SynsetKey objects and the values are Synset objects.
   */
  protected static Hashtable synsetTable = new Hashtable( );

  /** The name of the file that contain word sense frequency information. */
  protected static final String frequencyFile = "/wordnet/wn/dict/cntlist";

  /** Keeps track of the frequencies of word sense objects. The keys are
   *  WordSenseKey objects and the values are Integers.
   */
  protected static Hashtable frequencyTable = new Hashtable( );

  /** The name of the files that contain exceptions to the morpholocial
   *  transformation rules.
   */
  protected static final String[] baseFormExceptionFiles = { "",
    "/wordnet/wn/dict/noun.exc", "/wordnet/wn/dict/verb.exc",
    "/wordnet/wn/dict/adj.exc", "/wordnet/wn/dict/adv.exc"
  };

  /** Keeps track of the exceptions to the morphological transformation rules.
   *  The index to this array is the part of speech, the keys to the hashtables
   *  are strings representing the inflected forms, and the values are arrays
   *  of strings representing the base forms.
   */
  protected static Hashtable[] baseFormExceptionTables = new Hashtable[5];

  /** Contains the regular morphological transformation rules. */
  protected static String[][][] baseFormTable = { { },
    // NOUN
    {
      { "s", "" },
      { "ses", "s" },
      { "xes", "x" },
      { "zes", "z" },
      { "ches", "ch" },
      { "shes", "sh" }
    },
    // VERB
    {
      { "s", "" },
      { "ies", "y" },
      { "es", "e" },
      { "es", "" },
      { "ed", "e" },
      { "ed", "" },
      { "ing", "e" }
    },
    // ADJECTIVE
    {
      { "er", "" },
      { "est", "" },
      { "er", "e" },
      { "est", "e" }
    },
    // ADVERB
    { }
  };

  protected static final String libraryName = "wnjni";


  /* ***** Constructor ***** */


  public String getTitle( )
  {
    return "Ascii WNI v1.0";
  }

  /** Constructs and initializes an AsciiWordNetImpl object. */
  public AsciiWordNetImpl( )
  {
    System.loadLibrary( libraryName );

    try
    {
      for ( int i = Constants.NOUN; i <= Constants.ADVERB; i++ )
      {
        BufferedReader in = new BufferedReader( new FileReader(
          baseFormExceptionFiles[i]
        ) );
        baseFormExceptionTables[i] = new Hashtable( );
        for ( String line = in.readLine( ); line != null; line = in.readLine( ) )
        {
          StringTokenizer st = new StringTokenizer( line );
          String inflectedForm = st.nextToken( );
          String[] baseForms = new String[ st.countTokens( ) ];
          for ( int j = 0; j < baseForms.length; j++ )
            baseForms[j] = st.nextToken( );
          baseFormExceptionTables[i].put( inflectedForm, baseForms );
        }
      }
    }
    catch ( IOException ioe )
    {
      throw new WordNetException( "An I/O exception occurred while trying to read" +
        " the base form exception files." );
    }

    try
    {
      BufferedReader in = new BufferedReader( new FileReader( frequencyFile ) );
/*
      for ( String line = in.readLine( ); line != null; line = in.readLine( ) )
      {
        StringTokenizer st = new StringTokenizer( line );
        int frequency = Integer.parseInt( st.nextToken( ) );
        String senseKey = st.nextToken( );
        int sense = Integer.parseInt( st.nextToken( ) );
  
        st = new StringTokenizer( senseKey, "%:" );
        String word = st.nextToken( );
        int pos = Integer.parseInt( st.nextToken( ) );
  
        WordSenseKey key = lookupWordSenseKey( word, pos, sense );

        frequencyTable.put( key, new Integer( frequency ) );
      }
*/
    }
    catch ( IOException ioe )
    {
      throw new WordNetException( "An I/O exception ocurred while trying to read" +
        " the frequency count list file." );
    }

    WordNetImpl.setCurrentWNI( this );

  }


  /* ***** Methods Inherited from WordNetImpl ***** */


  public String[] lookupBaseForms( String word )
  {
    Vector formv = new Vector( );

    formv.addAll( lookupBaseForms( word, NOUN ) );
    formv.addAll( lookupBaseForms( word, VERB ) );
    formv.addAll( lookupBaseForms( word, ADJECTIVE ) );
    formv.addAll( lookupBaseForms( word, ADVERB ) );

    String[] forms = new String[ formv.size( ) ];
    for ( int i = 0; i < forms.length; i++ )
      forms[i] = ( String )formv.get( i );
    
    return forms;
  }

  /** Performs morphological analysis on the given word and returns all
   *  possible base forms in the given part of speech. This implementation
   *  first checks through an exceptions file whose name is specified in the
   *  field baseFormExceptionFile, and then tries to apply the string
   *  replacements specified in the field baseFormTable. Returns an array of
   *  Strings, each of which is a possible base form for the given word.
   */
  protected Vector lookupBaseForms( String word, int pos )
    throws WordNetException
  {
    
    if ( pos == Constants.SATELLITE ) pos = Constants.ADJECTIVE;
    String[] forms = ( String[] )baseFormExceptionTables[ pos ].get( word );
    if ( forms != null )
    {
      Vector vec = new Vector( );
      for ( int i = 0; i < forms.length; i++ )
        vec.add( forms[i] );
      return vec;
    }
    String[][] replacements = baseFormTable[ pos ];
    Vector formv = new Vector( );
    for ( int i = 0; i < replacements.length; i++ )
    {
      try
      {
        RE re = new RE( replacements[i][0] + "$" );
        if ( re.getMatch( word ) != null ) {
          String form = re.substitute( word, replacements[i][1] );
          if ( lookupSynsetAllSenses( form, pos ) != null )
            formv.add( form );
        }
      }
      catch( REException e )
      {
        throw new WordNetException( e.getMessage( ) );
      }
    }
    return formv;
  }

  public WordSenseKey lookupWordSenseKey( String key )
  {
    return new AsciiWordSenseKey( key );
  }

  public WordSenseKey lookupWordSenseKey( String word, int pos, int sense )
  {
    String hashKey = word + ":" + pos + ":" + sense;
    WordSenseKey wsk = ( WordSenseKey )wordSenseKeyTable.get( hashKey );
    if ( wsk == null )
    {
      CIndex ci = CIndex.lookup( toCanonicalForm( word ), pos );
      if ( ci == null )
        return null;

      if ( sense < 1 || sense > ci.offset.length )
        return null;

      word = ci.wd;
      int offset = ci.idxoffset;

      CSynset cs = CSynset.lookup( pos, ci.offset[ sense - 1 ], ci.wd );
      if ( cs == null )
        throw new WordNetException( "CSynset.lookup( ) returned null." );

      int lexid = cs.lexid[ cs.whichword - 1 ];

      int category = cs.fnum;
      String headword = ( cs.headword != null ? cs.headword : "" );
      int headid = cs.headsense;

      wsk = new AsciiWordSenseKey( word, pos, category, lexid, headword,
                                   headid, offset );
      wordSenseKeyTable.put( hashKey, wsk );
    }
    return wsk;
  }

  public SynsetKey lookupSynsetKey( String key )
  {
    return new AsciiSynsetKey( key );
  }

  public SynsetKey lookupSynsetKey( String word, int pos, int sense )
  {
    String hashKey = word + ":" + pos + ":" + sense;
    SynsetKey synsetKey = ( SynsetKey )synsetKeyTable.get( hashKey );
    if ( synsetKey == null )
    {
      CIndex ci = CIndex.lookup( toCanonicalForm( word ), pos );
      if ( ci == null )
        throw new WordNetException(
          "CIndex.lookup( " + toCanonicalForm( word ) + ", " + pos +
          " ) returned null!" );
      if ( sense < 1 )
        throw new WordNetException( "sense < 1" );
      synsetKey = new AsciiSynsetKey( pos, ci.offset[ sense - 1 ] );
      synsetKeyTable.put( hashKey, synsetKey );
    }
    return synsetKey;
  }
    

  /** Returns the word sense associated with the given key. Throws a
   *  WordNetException if the key is invalid. This method first checks an
   *  internal hashtable to see whether it already knows about a word sense
   *  with this key. If not, it uses the native code in CIndex to get the index
   *  for this word and pos, which it passes to convertWordSense( ) along with
   *  the sense number, to transfer the information into a new-style WordSense
   *  object.
   */
  public WordSense lookupWordSense( WordSenseKey key )
  {
    if ( key == null )
      throw new IllegalArgumentException( "WordSenseKey key was null!" );

    WordSense wordSense = ( WordSense )wordSenseTable.get( key );
    if ( wordSense != null )
      return wordSense;

    String word = ( ( AsciiWordSenseKey )key ).word;
    int pos = ( ( AsciiWordSenseKey )key ).pos;
    int offset = ( ( AsciiWordSenseKey )key ).offset;

    CIndex ci;

    if ( offset > 0 )
      ci = CIndex.lookupByOffset( pos, offset );
    else
      ci = CIndex.lookup( word, pos );

    CSenseIndex csi = CSenseIndex.lookup( key.toString( ) );

    if ( ci == null || csi == null )
      return null;

    wordSense = new WordSense( );

    wordSense.setPos( ci.pos );
    wordSense.setSense( csi.wnsense );
    wordSense.setPolysemy( ci.sense_cnt );
    wordSense.setFrequency( csi.tag_cnt );

    wordSense.setKey( key );
    wordSenseTable.put( key, wordSense );

    CSynset cs = CSynset.lookup(
      ci.pos, ci.offset[ csi.wnsense - 1 ], ci.wd
    );
    wordSense.setForm( cs.words[ cs.whichword - 1 ] );

    Vector relationv = new Vector( );
    for ( int i = 0; i < cs.ptrcount; i++ )
      if ( cs.pfrm[i] == csi.wnsense ) {
        CSynset dest = CSynset.lookup( cs.ppos[i], cs.ptroff[i], "" );
        if ( dest == null )
          throw new WordNetException( "CSynset.lookup( " + cs.ppos[i] + ", " +
            cs.ptroff[i] + " ) returned null!" );
        relationv.add(
          new LexicalRelation(
            cs.ptrtyp[i],
            lookupWordSenseKey(
              dest.words[ cs.pto[i]-1 ], dest.pos, dest.wnsns[ cs.pto[i]-1 ]
            )
          )
        );
      }
    LexicalRelation[] relations = new LexicalRelation[ relationv.size() ];
    for ( int i = 0; i < relations.length; i++ )
      relations[i] = ( LexicalRelation )relationv.get( i );
    
    wordSense.setRelations( relations );

    wordSense.setSynset( lookupSynset(
      lookupSynsetKey( ci.wd, ci.pos, csi.wnsense )
    ) );
    
    return wordSense;
  }

  /** Returns the synset associated with the given key. Throws an exception if
   *  the synset is missing. This method first checks an internal hashtable to
   *  see whether it already knows about a synset with this key. If not, it
   *  uses the native code in CSynset to get the synset, which it passes to
   *  convertSynset( ) to transfer the information from the old-style CSynset
   *  object to a new-style Synset object.
   */
  public Synset lookupSynset( SynsetKey key )
  {
    if ( key == null )
      throw new IllegalArgumentException( "SynsetKey key was null!" );

    Synset synset = ( Synset )synsetTable.get( key.toString( ) );

    if ( synset != null )
      return synset;

    int pos = ( ( AsciiSynsetKey )key ).pos;
    int offset = ( ( AsciiSynsetKey )key ).offset;

    CSynset cs = CSynset.lookup( pos, offset, "" );

    if ( cs == null )
      throw new WordNetException(
        "CSynset.lookup( " + pos + ", " + offset + ", \"\" ) returned null!"
      );

    synset = new Synset( );

    synset.setPos( cs.pos );
    /* Strip parentheses from gloss. */

    // Sometimes, by accident, a synset ends up without any gloss at all
    if ( cs.defn != null && cs.defn.length( ) >= 2 )
	synset.setGloss( cs.defn.substring( 1, cs.defn.length( ) - 1 ) );
    else
	synset.setGloss( "#GLOSS MISSING#" );

    synset.setCategory( cs.fnum );

    synset.setKey( key );
    synsetTable.put( key.toString( ), synset );

    WordSense[] synonyms = new WordSense[ cs.words.length ];
    for ( int i = 0; i < synonyms.length; i++ )
    {
      synonyms[i] = lookupWordSense(
        lookupWordSenseKey( cs.words[i], cs.pos, cs.wnsns[i] )
      );
      if ( synonyms[i] == null )
        throw new WordNetException(
          "lookupWordSense( " + cs.words[i] + ", " + cs.pos + ", " +
          cs.wnsns[i] + " ) returned null!"
        );
      synonyms[i].setSynset( synset );
    }
    synset.setSynonyms( synonyms );

    Vector relationv = new Vector( );
    for ( int i = 0; i < cs.ptrcount; i++ )
      if ( cs.pto[i] == SEMANTIC )
      {
        CSynset dest = CSynset.lookup( cs.ppos[i], cs.ptroff[i], "" );
        if ( dest == null )
          throw new WordNetException(
            "CSynset.lookup( " + cs.pos + ", " + cs.ptroff[i] +
            " ) returned null!"
          );
        relationv.add(
          new SemanticRelation(
            cs.ptrtyp[i],
            lookupSynsetKey( dest.words[0], dest.pos, dest.wnsns[0] )
          )
        );
      }
    SemanticRelation[] relations = new SemanticRelation[ relationv.size() ];
    for ( int i = 0; i < relations.length; i++ )
      relations[i] = ( SemanticRelation )relationv.get( i );
    synset.setRelations( relations );

    if ( cs.pos == Constants.VERB )
    {
      Vector framev = new Vector( );
      for ( int i = 0; i < cs.fcount; i++ )
        if ( cs.frmto[i] == Constants.SEMANTIC )
          framev.add( new Integer( cs.frmid[i] ) );

      int[] frames = new int[ framev.size( ) ];
      for ( int i = 0; i < frames.length; i++ )
        frames[i] = ( ( Integer )framev.get( i ) ).intValue( );
      
      synset.setFrames( frames );
    }

    return synset;

  }

  /** Returns the synsets associated with all sense of the given word. Uses
   *  native code in CIndex to get an old-style CIndex object. This object
   *  contains byte-offset information about synsets for each sense of the
   *  word. Each offset is passed to native code in CSynset, which returns
   *  an old-style CSynset object, which is converted to a new-style Synset
   *  object by convertSynset.
   */
  public Synset[] lookupSynsetAllSenses( String word, int pos )
  {
    if ( pos == ANY_POS )
    {
      Vector synsetv = new Vector( );
      for ( int i = NOUN; i <= ADVERB; i++ )
        synsetv.addAll( Arrays.asList( lookupSynsetAllSenses( word, i ) ) );
      Synset[] synsets = new Synset[ synsetv.size( ) ];
      for ( int i = 0; i < synsetv.size( ); i++ )
        synsets[i] = ( Synset )synsetv.get( i );
      return synsets;
    }

    CIndex ci = CIndex.lookup( toCanonicalForm( word ), pos );
    if ( ci == null ) return new Synset[ 0 ];
    Synset[] synsets = new Synset[ ci.off_cnt ];
    for ( int i = 0; i < synsets.length; i++ )
      synsets[i] = lookupSynset( new AsciiSynsetKey( pos, ci.offset[i] ) );
    return synsets;
  }
  
  public Synset[] lookupSynsetByGloss( String keyword, int pos )
  {
    throw new UnsupportedOperationException( );
  }
}
